Passed
Pull Request — 0.2 (#185)
by
unknown
04:36
created

ID3Util.js ➔ constructor   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
1
const iconv = require('iconv-lite')
2
const ID3Definitions = require('./ID3Definitions')
3
4
const ENCODINGS = [
5
    'ISO-8859-1', 'UTF-16', 'UTF-16BE', 'UTF-8'
6
]
7
8
module.exports.SplitBuffer = class SplitBuffer {
9
    constructor(value = null, remainder = null) {
10
        this.value = value
11
        this.remainder = remainder
12
    }
13
}
14
15
/**
16
 * Expects a buffer containing a string at the beginning that is terminated by a \0 character.
17
 * Returns a split buffer containing the bytes before and after null termination.
18
 */
19
module.exports.splitNullTerminatedBuffer = function(buffer, encodingByte = 0x00) {
20
    // UTF-16/BE always uses two bytes per character.
21
    // \0 is therefore encoded as [0x00, 0x00] instead of just [0x00].
22
    // We'll do a sliding window search, window size depends on encoding.
23
    const charSize = [0x01, 0x02].includes(encodingByte) ? 2 : 1
24
    for(let pos = 0; pos + charSize - 1 < buffer.length; pos += charSize) {
25
        if(buffer.readUIntBE(pos, charSize) === 0) {
26
            return new this.SplitBuffer(
27
                buffer.subarray(0, pos),
28
                buffer.subarray(pos + charSize)
29
            )
30
        }
31
    }
32
33
    return new this.SplitBuffer(null, buffer.subarray(0))
34
}
35
36
module.exports.terminationBuffer = function(encodingByte = 0x00) {
37
    if(encodingByte === 0x01 || encodingByte === 0x02) {
38
        return Buffer.alloc(2, 0x00)
39
    }
40
    return Buffer.alloc(1, 0x00)
41
}
42
43
module.exports.encodingFromStringOrByte = function(encoding) {
44
    if(ENCODINGS.indexOf(encoding) !== -1) {
45
        return encoding
46
    } else if(encoding > -1 && encoding < ENCODINGS.length) {
47
        encoding = ENCODINGS[encoding]
48
    } else {
49
        encoding = ENCODINGS[0]
50
    }
51
    return encoding
52
}
53
54
module.exports.stringToEncodedBuffer = function(str, encodingByte) {
55
    return iconv.encode(str, this.encodingFromStringOrByte(encodingByte))
56
}
57
58
module.exports.bufferToDecodedString = function(buffer, encodingByte) {
59
    return iconv.decode(buffer, this.encodingFromStringOrByte(encodingByte)).replace(/\0/g, '')
60
}
61
62
module.exports.getSpecOptions = function(frameIdentifier) {
63
    if(ID3Definitions.ID3_FRAME_OPTIONS[frameIdentifier]) {
64
        return ID3Definitions.ID3_FRAME_OPTIONS[frameIdentifier]
65
    }
66
67
    return {}
68
}
69
70
module.exports.isValidID3Header = function(buffer) {
71
    if(buffer.length < 10) {
72
        return false
73
    }
74
    if(buffer.readUIntBE(0, 3) !== 0x494433) {
75
        return false
76
    }
77
    if([0x02, 0x03, 0x04].indexOf(buffer[3]) === -1 || buffer[4] !== 0x00) {
78
        return false
79
    }
80
    return this.isValidEncodedSize(buffer.slice(6, 10))
81
}
82
83
module.exports.getFramePosition = function(buffer) {
84
    /* ID3v2 tags are ALWAYS at the beginning of the file (position 0)
85
     * This prevents false positives from "ID3" patterns in audio data
86
     * which can cause severe file corruption when removed
87
     */
88
    let framePosition = -1
89
    let frameHeaderValid = false
90
    
91
    // Only check position 0
92
    if (buffer.length >= 3 && buffer.slice(0, 3).toString() === "ID3") {
93
        framePosition = 0
94
        frameHeaderValid = this.isValidID3Header(buffer.slice(0, 10))
95
    }
96
97
    if(!frameHeaderValid) {
98
        return -1
99
    }
100
    return framePosition
101
}
102
103
/**
104
 * @param {Buffer} encodedSize
105
 * @return {boolean} Return if the header contains a valid encoded size
106
 */
107
module.exports.isValidEncodedSize = function(encodedSize) {
108
    // The size must not have the bit 7 set
109
    return ((
110
        encodedSize[0] |
111
        encodedSize[1] |
112
        encodedSize[2] |
113
        encodedSize[3]
114
    ) & 128) === 0
115
}
116
117
/**
118
 * ID3 header size uses only 7 bits of a byte, bit shift is needed
119
 * @param {number} size
120
 * @return {Buffer} Return a Buffer of 4 bytes with the encoded size
121
 */
122
module.exports.encodeSize = function(size) {
123
    const byte_3 = size & 0x7F
124
    const byte_2 = (size >> 7) & 0x7F
125
    const byte_1 = (size >> 14) & 0x7F
126
    const byte_0 = (size >> 21) & 0x7F
127
    return Buffer.from([byte_0, byte_1, byte_2, byte_3])
128
}
129
130
/**
131
 * Decode the size encoded in the ID3 header
132
 * @param {Buffer} encodedSize
133
 * @return {number} Return the decoded size
134
 */
135
module.exports.decodeSize = function(encodedSize) {
136
    return (
137
        (encodedSize[0] << 21) +
138
        (encodedSize[1] << 14) +
139
        (encodedSize[2] << 7) +
140
        encodedSize[3]
141
    )
142
}
143
144
module.exports.getFrameSize = function(buffer, decode, ID3Version) {
145
    let decodeBytes
146
    if(ID3Version > 2) {
147
        decodeBytes = [buffer[4], buffer[5], buffer[6], buffer[7]]
148
    } else {
149
        decodeBytes = [buffer[3], buffer[4], buffer[5]]
150
    }
151
    if(decode) {
152
        return this.decodeSize(Buffer.from(decodeBytes))
153
    } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
154
        return Buffer.from(decodeBytes).readUIntBE(0, decodeBytes.length)
155
    }
156
}
157
158
module.exports.parseTagHeaderFlags = function(header) {
159
    if(!(header instanceof Buffer && header.length >= 10)) {
160
        return {}
161
    }
162
    const version = header[3]
163
    const flagsByte = header[5]
164
    if(version === 3) {
165
        return {
166
            unsynchronisation: !!(flagsByte & 128),
167
            extendedHeader: !!(flagsByte & 64),
168
            experimentalIndicator: !!(flagsByte & 32)
169
        }
170
    }
171 View Code Duplication
    if(version === 4) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
172
        return {
173
            unsynchronisation: !!(flagsByte & 128),
174
            extendedHeader: !!(flagsByte & 64),
175
            experimentalIndicator: !!(flagsByte & 32),
176
            footerPresent: !!(flagsByte & 16)
177
        }
178
    }
179
    return {}
180
}
181
182
module.exports.parseFrameHeaderFlags = function(header, ID3Version) {
183
    if(!(header instanceof Buffer && header.length === 10)) {
184
        return {}
185
    }
186
    const flagsFirstByte = header[8]
187
    const flagsSecondByte = header[9]
188 View Code Duplication
    if(ID3Version === 3) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
189
        return {
190
            tagAlterPreservation: !!(flagsFirstByte & 128),
191
            fileAlterPreservation: !!(flagsFirstByte & 64),
192
            readOnly: !!(flagsFirstByte & 32),
193
            compression: !!(flagsSecondByte & 128),
194
            encryption: !!(flagsSecondByte & 64),
195
            groupingIdentity: !!(flagsSecondByte & 32),
196
            dataLengthIndicator: !!(flagsSecondByte & 128)
197
        }
198
    }
199
    if(ID3Version === 4) {
200
        return {
201
            tagAlterPreservation: !!(flagsFirstByte & 64),
202
            fileAlterPreservation: !!(flagsFirstByte & 32),
203
            readOnly: !!(flagsFirstByte & 16),
204
            groupingIdentity: !!(flagsSecondByte & 64),
205
            compression: !!(flagsSecondByte & 8),
206
            encryption: !!(flagsSecondByte & 4),
207
            unsynchronisation: !!(flagsSecondByte & 2),
208
            dataLengthIndicator: !!(flagsSecondByte & 1)
209
        }
210
    }
211
    return {}
212
}
213
214
module.exports.processUnsynchronisedBuffer = function(buffer) {
215
    const newDataArr = []
216
    if(buffer.length > 0) {
217
        newDataArr.push(buffer[0])
218
    }
219
    for(let i = 1; i < buffer.length; i++) {
220
        if(buffer[i - 1] === 0xFF && buffer[i] === 0x00)
221
            continue
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
222
        newDataArr.push(buffer[i])
223
    }
224
    return Buffer.from(newDataArr)
225
}
226
227
module.exports.getPictureMimeTypeFromBuffer = function(pictureBuffer) {
228
    if (pictureBuffer.length > 3 && pictureBuffer.compare(Buffer.from([0xff, 0xd8, 0xff]), 0, 3, 0, 3) === 0) {
229
        return "image/jpeg"
230
    } else if (pictureBuffer > 8 && pictureBuffer.compare(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), 0, 8, 0, 8) === 0) {
231
        return "image/png"
232
    } else {
233
        return null
234
    }
235
}
236